package com.spring.sso.config.security.filter.oauth2;

import com.spring.sso.config.common.AuthenticationConfig;
import com.spring.sso.config.security.adapter.ServerWebSecurityConfigureAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.util.Map;

/**
 * --> 开启OAUth2授权服务器配置策略
 *      例子: 登陆B站
 *
 *          1、访问B站登陆页: https://passport.bilibili.com/login?from_spm_id=333.851.top_bar.login_window
 *
 *              1.1、点击第三方登陆微信图标, 向微信发起授权登陆
 *
 *          2、向微信发起登陆请求:
 *
 *              2.1、https://open.weixin.qq.com/connect/qrconnect?
 *                      appid=wxafc256bf83583323    &
 *                      redirect_uri=https%3A%2F%2Fpassport.bilibili.com%2Flogin%2Fsnsback%3Fsns%3Dwechat%26%26state%3Df595b3f02b1111ecb2db1e91fd2cdcfb     &
 *                      response_type=code      &
 *                      scope=snsapi_login      &
 *                      state=authorize#wechat_redirect
 *
 *              2.2、扫码成功后: 此时浏览器请求:   https://lp.open.weixin.qq.com/connect/l/qrconnect?uuid=081BxHfG3qXWkl25&last=404&_=1634011828568
 *
 *              2.3、手机上点击同一授权
 *
 *          3、B站最终请求地址：
 *              https://passport.bilibili.com/register/snsback.html?
 *                  sns=wechat  &
 *                  snsUid=oiRWrwhvVQMqi_WnpDdQMBzFn8Vw &
 *                  snsAccessToken=49_ZTQtxVMmeAOvjMCRvT-Tly-eZdBypHn_xsfenoJWsiY3zVmPAU58ul3s08wcxFVCzI6SzzqGWFyr2rHohOdeZc0fJD9SAQw3XcPnUMcMHxk   &
 *                  snsAccessExpires=7200   &
 *                  csrf=f595b3f02b1111ecb2db1e91fd2cdcfb   &
 *                  gourl=https%3A%2F%2Fwww.bilibili.com%2F#/
 *
 *      登陆: 单点登陆
 *
 *      认证与授权: oauth2 --> 认证授权的过程一定发生在登陆过后, 即用户作为资源的拥有者授权访问第三方应用时，如果希望指定自身的某个应用授权登陆, 那么需要先登陆成功执行授权的应用方可进行
 *
 *          对其他第三方系统的授权
 *
 *              1、Sass系统, 开放平台系统, 餐饮系统
 *
 *                  登陆: 需要在此三个系统之间实现单点登陆, 即登陆任意一个系统后, 其他系统免登陆
 *
 *                  认证与授权: 餐饮系统登陆成功, 内部有访问开放平台数据资源时, 此时就需要认证与授权，由于已经发生过登陆, 此时就是按照oauth2进行认证授权
 *
 * @author : pangfuzhong
 * @description
 * @date : 2021/10/9 09:34
 */
@Configuration
@EnableAuthorizationServer
public class Oauth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationConfig authenticationConfig;

    /**
     * ClientRegistration: authorization code授权许可模式下，client在拿到authorization code后需要向authorization server申请access token。
     *  在此过程中，为了安全，authorization server需要验证client的身份，同时client当然需要提交身份验证所需的信息。因此，在整个流程的第一步是client先
     *  要向authorization server注册，并生成相关的身份验证信息，这个就是client registration的含义。
     *
     * */
    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        ClientRegistration github = ClientRegistration.withRegistrationId("github")  // (1)
            .clientId("b70db1c385ada821d268")  // (2)
            .clientSecret("f899ba22ef7c8ee3c0f6e19564bf810b3123daa4")  // (3)
            .clientAuthenticationMethod(ClientAuthenticationMethod.POST)  // (4)
            // 第三方alipay认证成功后会回调该地址进行code拼接返回
            .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")  // (5)
            .clientName("登陆Github")       // (6)
            .tokenUri("https://github.com/oauth/token")  // (7)
            // 第三方登陆认证alipay提供的登陆认证地址
            .authorizationUri("https://github.com/login/oauth/authorize")  // (8)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)  // (9)
            .scope("trust")  // (10)
            .userNameAttributeName("username")  // (11)
            .userInfoUri("http://192.168.14.239:8081/api/v3/user")  // (12)
            .jwkSetUri("")  // (13)
            .build();

        // 2、基于Gitee认证方式
        ClientRegistration gitee = ClientRegistration.withRegistrationId("gitee")
            .clientId("8a9a98f7815e59445875e8710dcc2de6ce81a1ec28cf1cd33fa380c09ec68d5f")
            .clientSecret("d9fc7121c4b1e8aca4f8285c5f430dc4462a7e37b13e085058c59b4b4f464a76")
            .clientAuthenticationMethod(ClientAuthenticationMethod.POST)
            // 向gitee提供client_id以及client_secret认证成功后重定向该redirectUriTemplate指定的路径，并在该路径后放置code(授权码)以及state
            .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")  // (5)
            .clientName("登陆gitee")       // (6)
            // 认证成功后的code换accessToken的接口
            .tokenUri("https://gitee.com/oauth/token")  // (7)
            // 向gitee进行授权码获取的接口
            .authorizationUri("https://gitee.com/oauth/authorize")  // (8)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)  // (9)
            .scope("user_info")  // (10)
            .userNameAttributeName("name")  // (11)
            .userInfoUri("https://gitee.com/api/v5/user")  // (12)
            .jwkSetUri("")  // (13)
            .build();

        ClientRegistration ssoServer = ClientRegistration.withRegistrationId("ssoServer")  // (1)
            .clientId("ssoServer")  // (2)
            .clientSecret("A123456")  // (3)
            .clientAuthenticationMethod(ClientAuthenticationMethod.POST)  // (4)
            // 第三方alipay认证成功后会回调该地址进行code拼接返回
            .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")  // (5)
            .clientName("自定义登陆")       // (6)
            .tokenUri("http://192.168.14.239:8081/oauth/token")  // (7)
            // 第三方登陆认证alipay提供的登陆认证地址
            .authorizationUri("http://192.168.14.239:8081/oauth/authorize?action=http://192.168.14.239:8081/save")  // (8)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)  // (9)
            .scope("trust")  // (10)
            .userNameAttributeName("username")  // (11)
            .userInfoUri("http://192.168.14.239:8081/api/v3/user")  // (12)
            .jwkSetUri("")  // (13)
            .build();

        return new InMemoryClientRegistrationRepository(github, gitee, ssoServer);
    }

    /**
     * 配置授权服务器的安全性
     * */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // 如果追加passEncoder配置将会导致应用起不来
        security
            // 开启/oauth/token_key验证端口认证权限访问
            .tokenKeyAccess("permitAll()")
            //  开启/oauth/check_token验证端口认证权限访问
            .checkTokenAccess("permitAll()")
            //允许表单认证
            .allowFormAuthenticationForClients();
        security.passwordEncoder(ServerWebSecurityConfigureAdapter.encoder);
    }

    /**
     * 配置客户端属性
     *
     *  这个如果配置支持allowFormAuthenticationForClients的，且url中有client_id和client_secret的会走ClientCredentialsTokenEndpointFilter来保护
     *     如果没有支持allowFormAuthenticationForClients或者有支持但是url中没有client_id和client_secret的，走basic认证保护
     *      *
     *      * @param clients
     *      * @throws Exception
     *
     * */
    @Bean
    public ClientDetailsService clientDetailsService() {
        return new JdbcClientDetailsService(this.authenticationConfig.getDataSource());
    }

    /**
     * 配置基于jdbc方式的oauth_client_details 应用数据配置的读取
     * */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // JdbcClientDetailsService --> 操作表 oauth_client_details
        clients.withClientDetails(clientDetailsService());
    }

    /**
     * 配置授权服务器端点的非安全特性:如token store、token
     *  /oauth/authorize：授权端点
     * /oauth/token：令牌端点
     * /oauth/confirm_access：用户确认授权提交端点
     * /oauth/error：授权服务错误信息端点
     * /oauth/check_token：用于资源服务访问的令牌解析端点
     * /oauth/token_key：提供公有密匙的端点，如果使用JWT令牌的话
     * */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        /*List<TokenEnhancer> delegates = new ArrayList<>();
        delegates.add(jwtTokenEnhancer);
        delegates.add(accessTokenConverter());
        enhancerChain.setTokenEnhancers(delegates); //配置JWT的内容增强器*/
        endpoints.pathMapping("forward:/oauth/confirm_access", "forward: /oauth/saas/sso/confirm_access")
            // 基于 response_type = code [授权码模式] 下配置授权码的存储处理
            .authorizationCodeServices(new JdbcAuthorizationCodeServices(this.authenticationConfig.getDataSource()))
            .userDetailsService(this.authenticationConfig.getUserDetailsService())
            // 配置token生成后存储于redis
            .tokenStore(new RedisTokenStore(this.authenticationConfig.getRedisTemplate().getConnectionFactory()))
            // 配置token生成服务类
            .tokenServices(tokenServices())
            // 配置用户批准存储服务类 (操作oauth_approval表)
            .approvalStore(approvalStore())
            .accessTokenConverter(accessTokenConverter());
        // 此处设置ClientDetailsService后会在该方法返回后覆盖, 所以设置没有意义
        //endpoints.setClientDetailsService(new JdbcClientDetailsService(this.authenticationConfig.getDataSource()));
    }

    //授权信息保存策略
    @Bean
    public ApprovalStore approvalStore(){
        return new JdbcApprovalStore(this.authenticationConfig.getDataSource());
    }

    @Bean
    AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices services = new DefaultTokenServices();
        services.setClientDetailsService(clientDetailsService());
        services.setSupportRefreshToken(true);
        services.setTokenStore(new RedisTokenStore(this.authenticationConfig.getRedisTemplate().getConnectionFactory()));
        services.setAccessTokenValiditySeconds(60 * 60 * 2);
        services.setRefreshTokenValiditySeconds(60 * 60 * 24 * 3);
        return services;
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() throws NoSuchAlgorithmException {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        jwtAccessTokenConverter.setKeyPair(keyPairGenerator.generateKeyPair());
        return jwtAccessTokenConverter;
    }
}
